Esse tutorial é uma adaptação do tutorial original que pode ser encontrado em: https://www.datacamp.com/community/tutorials/keras-r-deep-learning

Keras é um pacote que oferece funções para o estudo sobre modelos abordando deep learning no R.

Esse tutorial terá como foco o MLP (Multi-Layer Perceptron), um tipo de Rede Neural Artificial que possui pelo menos três camadas internas compostas por nós (neurônios).

Instalando o pacote

Primeiro, importaremos o pacote devtools para poder instalar os pacotes que serão necessários:

library(devtools)

Em seguida, são importados o keras e tensorflow, para isso é necessário tê-los instalados:

# install_github("rstudio/tensorflow")
# install_github("rstudio/keras")

library(keras)
library(tensorflow)

# install_keras()

Carregando os dados

Os dados podem ser carregados de 3 maneiras diferentes:

Utilizando os datasets do keras:

mnist <- dataset_mnist()

cifar10 <- dataset_cifar10()

imdb <- dataset_imdb()

Criando nosso próprio dataset com dados aleatórios a partir da função matrix():

dados <- matrix(rexp(1000*784), nrow = 1000, ncol = 784)

labels <- matrix(round(runif(1000 * 10, min = 0, max = 9)),
                 nrow = 1000, ncol = 10)

Importando de um arquivo CSV (ou outro formato):

# Carregando o arquivo na variável iris: 
iris <- read.csv(url("http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"), header = FALSE)

# Cabeçalho do dataset
head(iris)
##    V1  V2  V3  V4          V5
## 1 5.1 3.5 1.4 0.2 Iris-setosa
## 2 4.9 3.0 1.4 0.2 Iris-setosa
## 3 4.7 3.2 1.3 0.2 Iris-setosa
## 4 4.6 3.1 1.5 0.2 Iris-setosa
## 5 5.0 3.6 1.4 0.2 Iris-setosa
## 6 5.4 3.9 1.7 0.4 Iris-setosa
# Estrutura
str(iris)
## 'data.frame':    150 obs. of  5 variables:
##  $ V1: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ V2: num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ V3: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ V4: num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ V5: Factor w/ 3 levels "Iris-setosa",..: 1 1 1 1 1 1 1 1 1 1 ...
# Dimensões
dim(iris)
## [1] 150   5

Utilizaremos esse dataset que contém dados sobre as dimensões de flores de íris e suas respectivas classificações. Mais detalhes em: http://archive.ics.uci.edu/ml/datasets/Iris

Exploração dos dados

É importante saber que todas as flores possuem sépalas e pétalas. Sépalas tem tipicamente um tom esverdeado, enquanto as pétalas são coloridas. Isso é diferente nas íris, como pode ser visto nas imagens:

Adicionaremos nomes para as colunas com a função names() e utilizaremos a função plot() para visualizar a correlação entre o comprimento e a largura das pétalas:

# Atribuindo nomes às colunas
names(iris) <- c("Sepala.Comprimento", "Sepala.Largura", "Petala.Comprimento", "Petala.Largura", "Especie")

# Plotando o gráfico de correlação
plot(iris$Petala.Comprimento,
     iris$Petala.Largura,
     pch = 21,
     bg = c("red", "green3", "blue")[unclass(iris$Especie)],
     xlab = "Comprimento da Petala",
     ylab = "Largura da Petala")

# Correlação geral entre os dois atributos
cor(iris$Petala.Comprimento, iris$Petala.Largura)
## [1] 0.9627571

A função unclass() foi usada para mapear os nomes das espécies para valores numéricos (1, 2 e 3).

Pelo gráfico, é possível observar que há uma correlação significativa entre o comprimento e a largura da pétala e seu valor é de 0.9627571.

Precisamos verificar os outros atributos, para isso utilizamos a função cor() com todos os atributos. Além disso, utilizamos a função corrplot() do pacote de mesmo nome para ter uma melhor visão das correlações:

# Salvando a correção geral em M
M <- cor(iris[,1:4])

# Importando o pacote corrplot
library(corrplot)

# Plotando o gráfico de correlações com o método de círculos
corrplot(M, method = "circle")

Pré-processamento dos dados

Antes de construírmos o modelo, é necessário termos certeza de que os dados estão limpos, normalizados e divididos entre treino e teste.

Como estamos fazendo uso de um dataset do UCI Machine Learning Repository, podemos esperar que esses dados já estejam prontos para uso, ou seja, limpos e normalizados. Checaremos isso a seguir.

Normalizando os dados:

Iremos utilizar a função normalize() do keras. Para isso, precisamos ter os dados dispostos em uma matriz:

# Convertendo os nomes das espécies em valores numéricos
iris[,5] <- as.numeric(iris[,5]) -1

# Convertendo os dados em uma matriz
iris <- as.matrix(iris)

# Excluindo os nomes das colunas
dimnames(iris) <- NULL

# Normalizando os dados
irisNormalizado <- normalize(iris[,1:4])

# Mostrando o sumário
summary(iris)
##        V1              V2              V3              V4       
##  Min.   :4.300   Min.   :2.000   Min.   :1.000   Min.   :0.100  
##  1st Qu.:5.100   1st Qu.:2.800   1st Qu.:1.600   1st Qu.:0.300  
##  Median :5.800   Median :3.000   Median :4.350   Median :1.300  
##  Mean   :5.843   Mean   :3.054   Mean   :3.759   Mean   :1.199  
##  3rd Qu.:6.400   3rd Qu.:3.300   3rd Qu.:5.100   3rd Qu.:1.800  
##  Max.   :7.900   Max.   :4.400   Max.   :6.900   Max.   :2.500  
##        V5   
##  Min.   :0  
##  1st Qu.:0  
##  Median :1  
##  Mean   :1  
##  3rd Qu.:2  
##  Max.   :2
summary(irisNormalizado)
##        V1               V2               V3               V4         
##  Min.   :0.6539   Min.   :0.2384   Min.   :0.1678   Min.   :0.01473  
##  1st Qu.:0.7153   1st Qu.:0.3267   1st Qu.:0.2509   1st Qu.:0.04873  
##  Median :0.7549   Median :0.3544   Median :0.5364   Median :0.16415  
##  Mean   :0.7516   Mean   :0.4048   Mean   :0.4550   Mean   :0.14096  
##  3rd Qu.:0.7884   3rd Qu.:0.5252   3rd Qu.:0.5800   3rd Qu.:0.19753  
##  Max.   :0.8609   Max.   :0.6071   Max.   :0.6370   Max.   :0.28042
Separando os dados:

Precisamos separar nosso dataset em treino e teste. Para isso, definimos um peso para cada uma das duas partições utilizando a função sample():

# Define o tamanho das amostras
ind <- sample(2, nrow(iris), replace = TRUE, prob = c(0.67, 0.33))

# Divide os dados
iris.treino <- iris[ind==1, 1:4]
iris.teste <- iris[ind==2, 1:4]

# Divide o atributo de classificação
iris.treinoalvo <- iris[ind==1, 5]
iris.testealvo <- iris[ind==2, 5]
Codificação One-Hot

Utilizaremos a função to_categorical() para converter os arrays com o atributo alvo para uma matriz de booleanos, que indica a qual classe pertence cada dado:

iris.treinoRotulos <- to_categorical(iris.treinoalvo)

iris.testeRotulos <- to_categorical(iris.testealvo)

Construindo o Modelo

Nosso objetivo é classificar flores de íris como versicolor, setosa ou virginica. Esse problema é conhecido como um perceptron multi-camadas, em que temos camadas internas totalmente conectadas com uma função de ativação, que nesse caso será a ReLU. Além disso, utilizaremos a função softmax para a camada de saída, onde teremos valores na faixa de 0 a 1.

# Iniciando o modelo sequencial
modelo <- keras_model_sequential()

# Adicionando as camadas internas e de saída
modelo %>% layer_dense(units = 8, activation = "relu",
                      input_shape = c(4)) %>% layer_dense(units = 3, activation = "softmax")

Visualizando o modelo:

# Sumário
summary(modelo)
## ___________________________________________________________________________
## Layer (type)                     Output Shape                  Param #     
## ===========================================================================
## dense_1 (Dense)                  (None, 8)                     40          
## ___________________________________________________________________________
## dense_2 (Dense)                  (None, 3)                     27          
## ===========================================================================
## Total params: 67
## Trainable params: 67
## Non-trainable params: 0
## ___________________________________________________________________________
# Configuração
get_config(modelo)
## [{'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'batch_input_shape': (None, 4), 'dtype': 'float32', 'units': 8, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense_2', 'trainable': True, 'units': 3, 'activation': 'softmax', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}]
# Configuração das camadas
get_layer(modelo, index = 1)
## <keras.layers.core.Dense>
# Camadas
modelo$layers
## [[1]]
## <keras.layers.core.Dense>
## 
## [[2]]
## <keras.layers.core.Dense>
# Entradas
modelo$inputs
## [[1]]
## Tensor("dense_1_input:0", shape=(?, 4), dtype=float32)
# Saídas
modelo$outputs
## [[1]]
## Tensor("dense_2/Softmax:0", shape=(?, 3), dtype=float32)

Compilando e treinando o modelo

# Compilando o modelo
modelo %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

Os parâmetros loss e optimizer são necessários para treinar o modelo. O primeiro representa a função de perda utilizada, enquanto o segundo significa o algoritmo de otimização.

O próximo passo é treinar o modelo com os dados de treino, por 200 épocas, em lotes de 5 amostras:

# Treinando o modelo e guardando em 'treinamento'
treinamento <- modelo %>% fit(
     iris.treino, 
     iris.treinoRotulos, 
     epochs = 200, 
     batch_size = 5, 
     validation_split = 0.2
 )

Visualizando o treinamento do modelo

O treino pode ser visto abaixo com detalhes:

# Plotando o gráfico
plot(treinamento)

Os gráficos acima parecem um pouco confusos. Temos um representando a perda e outro a acurácia. Veremos de forma mais clara abaixo:

# Plotando a perda dos dados de treino
plot(treinamento$metrics$loss, main="Perda", xlab = "Epocas", ylab="Perda", col="blue", type="l")

# Plotando a perda dos dados de teste
lines(treinamento$metrics$val_loss, col="green")

# Adicionando legendas
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

# Plot the accuracy of the training data 
plot(treinamento$metrics$acc, main="Acurácia", xlab = "Epocas", ylab="Acurácia", col="blue", type="l")

# Plot the accuracy of the validation data
lines(treinamento$metrics$val_acc, col="green")

# Add Legend
legend("bottomright", c("train","test"), col=c("blue", "green"), lty=c(1,1))

Importante:

Predição de rótulos de novos dados

Agora que temos nosso modelo compilado e treinado, podemos fazer a predição dos dados de teste a partir da função predict:

# Predição das classes do dataset de teste
classes <- modelo %>% predict_classes(iris.teste, batch_size = 128)

# Matriz de confusão para verificar as predições
table(iris.testealvo, classes)
##               classes
## iris.testealvo  0  1  2
##              0 13  0  0
##              1  0 14  1
##              2  0  3 11

Avaliando o modelo

Podemos ver a partir de uma pontuação como nosso modelo se saiu:

# Guardando a avaliação do modelo
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)

# Imprimindo o resultado
print(score)
## $loss
## [1] 0.1946697
## 
## $acc
## [1] 0.9047619

Ajustando o modelo

Na maioria das vezes precisamos fazer ajustes para o modelo a partir dos resultados obtidos. Faremos três ajustes a fim de melhorar nosso modelo:

  1. Adicionando camadas:

Veremos o que acontecerá caso adicionássemos mais uma camada ao modelo:

# Inicializando o modelo sequencial
modelo <- keras_model_sequential() 

# Adicionando camadas
modelo %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 5, activation = 'relu') %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compilando
modelo %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

# Treinando
treinamento <- modelo %>% fit(
     iris.treino, iris.treinoRotulos, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)

# Imprimindo o score
print(score)
## $loss
## [1] 0.1538553
## 
## $acc
## [1] 0.9285714

Visualizando a perda e acurácia do novo modelo:

# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

  1. Nós internos:

Veremos o efeito de adicionar mais nós nas camadas internas do nosso modelo:

# Inicializando o modelo sequencial
modelo <- keras_model_sequential() 

# Adicionando camadas
modelo %>% 
  layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>% 
  layer_dense(units = 3, activation = 'softmax')

# Compilando
modelo %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

# Treinando
treinamento <- modelo %>% fit(
     iris.treino, iris.treinoRotulos, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)

# Imprimindo o score
print(score)
## $loss
## [1] 0.1591196
## 
## $acc
## [1] 0.9285714

É importante saber que aumentar a quantidade de nós internos nem sempre significa melhorar a performance do modelo, podendo gerar overfitting. No nosso caso, com um dataset pequeno, o ideal é usar uma rede com poucos nós.

Visualizando o efeito:

# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

  1. Parâmetros de otimização:

Tentaremos utilizar outro algoritmo de otimização (SGD), até agora estávamos usando o ADAM:

# Inicializando o modelo sequencial
modelo <- keras_model_sequential() 

# Adicionando camadas
modelo %>% 
  layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>% 
  layer_dense(units = 3, activation = 'softmax')

# Definindo o algoritmo com learning rate 0.01 (taxa de aprendizado)
sgd <- optimizer_sgd(lr = 0.01)

# Usando o algoritmo para compilar o modelo
modelo %>% compile(optimizer = sgd,
                   loss='categorical_crossentropy',
                   metrics='accuracy')

# Treinando
treinamento <- modelo %>% fit(
     iris.treino, iris.treinoRotulos, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)

# Iprime as métricas de perda e acurácia
print(score)
## $loss
## [1] 0.1831878
## 
## $acc
## [1] 0.9047619

Além de alterar o algoritmo de otimização, é possível também alterar a taxa de aprendizado do algoritmo. Essa é uma das técnicas mais comuns de ajuste de modelo.

Veremos os efeitos dessa mudança:

# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))

Salvando, carregando ou exportando o modelo

Podemos salvar nosso modelo para utilizá-lo futuramente:

# Salvando o modelo
save_model_hdf5(modelo, "meu_modelo.h5")

# Carregando um modelo salvo
modelo <- load_model_hdf5("meu_modelo.h5")

É possível também salvar os pesos do modelo:

save_model_weights_hdf5(modelo, "pesos_modelo.h5")

modelo %>% load_model_weights_hdf5("pesos_modelo.h5")

Também podemos exportar o modelo para JSON ou YAML:

json_string <- model_to_json(modelo)
modelo <- model_from_json(json_string)

yaml_string <- model_to_yaml(modelo)
modelo <- model_from_yaml(yaml_string)